# SPDX-License-Identifier: Apache-2.0

# 这个功能写 dict 到输出参数
#
# 示例:
#    seminix_get_parse_args(foo ${ARGN})
#    print(foo_STRIP_PREFIX) # foo_STRIP_PREFIX might be set to 1
function(seminix_get_parse_args return_dict)
  foreach(x ${ARGN})
    if(DEFINED single_argument)
      set(${single_argument} ${x} PARENT_SCOPE)
      unset(single_argument)
    else()
      if(x STREQUAL STRIP_PREFIX)
        set(${return_dict}_STRIP_PREFIX 1 PARENT_SCOPE)
      elseif(x STREQUAL NO_SPLIT)
        set(${return_dict}_NO_SPLIT 1 PARENT_SCOPE)
      elseif(x STREQUAL DELIMITER)
        set(single_argument ${return_dict}_DELIMITER)
      endif()
    endif()
  endforeach()
endfunction()

# 如果支持该标志, 则为语言 'lang' 写 1 到输出变量 'ok', 否则写入 0.
#
# lang 必须是 C (CXX 在这里可以支持, 不过我们不需要)
#
# TODO: 支持 ASM
#
# 示例:
# check_compiler_flag(C "-Wall" my_check)
# print(my_check) # my_check is now 1
function(check_compiler_flag lang option ok)
  if(NOT DEFINED CMAKE_REQUIRED_QUIET)
    set(CMAKE_REQUIRED_QUIET 1)
  endif()

  string(MAKE_C_IDENTIFIER
    check${option}_${lang}_${CMAKE_REQUIRED_FLAGS}
    ${ok}
  )

  if(${lang} STREQUAL C)
    check_c_compiler_flag("${option}" ${${ok}})
  else()
    check_cxx_compiler_flag("${option}" ${${ok}})
  endif()

  if(${${${ok}}})
    set(ret 1)
  else()
    set(ret 0)
  endif()

  set(${ok} ${ret} PARENT_SCOPE)
endfunction()

function(seminix_check_compiler_flag_hardcoded lang option check exists)
  # 某些选项会在测试期间产生警告而不是错误. 通过工具链特定的阻塞列表排除它们.
  if((${lang} STREQUAL CXX) AND ("${option}" IN_LIST CXX_EXCLUDED_OPTIONS))
    set(check 0 PARENT_SCOPE)
    set(exists 1 PARENT_SCOPE)
  else()
    # There does not exist a hardcoded check for this option.
    set(exists 0 PARENT_SCOPE)
  endif()
endfunction()

# seminix_check_compiler_flag 是 seminix 工具链基础设施的一部分.
# 它应该在测试工具链能力时使用, 通常应该在下面 function 中使用:
#
# check_compiler_flag
# check_c_compiler_flag
#
# 它是作为 check_compiler_flag 的包装器实现的, check_compiler_flag
# 再次封装了 CMake-builtin 的 check_c_compiler_flag.
#
# 检查工具链上的标志兼容性需要时间, 所以我们将能力测试结果缓存到
# USER_CACHE_DIR 中.
function(seminix_check_compiler_flag lang option check)
  # 在进行自动化测试之前, 检查该选项是否被任何硬编码检查所覆盖.
  seminix_check_compiler_flag_hardcoded(${lang} "${option}" check exists)
  if(exists)
    set(check ${check} PARENT_SCOPE)
    return()
  endif()

  # 本地缓存目录
  set_ifndef(
    SEMINIX_TOOLCHAIN_CAPABILITY_CACHE_DIR
    ${USER_CACHE_DIR}/ToolchainCapabilityDatabase
  )

  set(cacheformat 3)

  set(key_string "")
  set(key_string "${key_string}${cacheformat}_")
  set(key_string "${key_string}${TOOLCHAIN_SIGNATURE}_")
  set(key_string "${key_string}${lang}_")
  set(key_string "${key_string}${option}_")
  set(key_string "${key_string}${CMAKE_REQUIRED_FLAGS}_")

  string(MD5 key ${key_string})

  # 检测缓存
  set(key_path ${SEMINIX_TOOLCHAIN_CAPABILITY_CACHE_DIR}/${key})
  if(EXISTS ${key_path})
    file(READ
      ${key_path}   # File to be read
      key_value     # Output variable
      LIMIT 1       # Read at most 1 byte ('0' or '1')
    )

    set(${check} ${key_value} PARENT_SCOPE)
    return()
  endif()

  # ok. 缓存中不存在对应选项, 我们测试并缓存结果
  #
  # -Wno-<warning> 不能通过 check_compiler_flag 测试, 它们总是会通过,
  # 但是 -W<warning> 可以进行测试, 因此所有 -Wno-<warning> 使用 -W<warning> 替代
  if("${option}" MATCHES "-Wno-(.*)")
    set(possibly_translated_option -W${CMAKE_MATCH_1})
  else()
    set(possibly_translated_option ${option})
  endif()

  check_compiler_flag(${lang} "${possibly_translated_option}" inner_check)

  set(${check} ${inner_check} PARENT_SCOPE)

  # 填充缓存
  if(NOT (EXISTS ${key_path}))
    string(RANDOM LENGTH 8 tempsuffix)

    file(WRITE
      "${key_path}_tmp_${tempsuffix}"
      ${inner_check}
    )
    file(RENAME
      "${key_path}_tmp_${tempsuffix}" "${key_path}"
    )

    file(APPEND
      ${SEMINIX_TOOLCHAIN_CAPABILITY_CACHE_DIR}/log.txt
      "${inner_check} ${key} ${key_string}\n"
    )
  endif()
endfunction()

# 如果第一个选项不支持, 支持使用第二个选项作为替补.
function(target_cc_option_fallback target scope option1 option2)
  # 假设适用于 C/CXX 的标志也适用于 ASM.
  seminix_check_compiler_flag(C ${option1} check)
  if(${check})
    target_compile_options(${target} ${scope}
      $<$<COMPILE_LANGUAGE:C>:${option1}>
      $<$<COMPILE_LANGUAGE:ASM>:${option1}>
    )
  elseif(option2)
    target_compile_options(${target} ${scope}
      $<$<COMPILE_LANGUAGE:C>:${option2}>
      $<$<COMPILE_LANGUAGE:ASM>:${option2}>
    )
  endif()
endfunction()

function(target_cc_option target scope option)
  target_cc_option_fallback(${target} ${scope} ${option} "")
endfunction()

function(target_cc_options target scope option)
  target_cc_option_fallback(${target} ${scope} ${option} "")
  foreach(arg ${ARGN})
    target_cc_option_fallback(${target} ${scope} ${arg} "")
  endforeach()
endfunction()

# import_kconfig(<prefix> <kconfig_fragment> [<keys>])
#
# Parse a KConfig fragment (typically with extension .config) and
# introduce all the symbols that are prefixed with 'prefix' into the
# CMake namespace. List all created variable names in the 'keys'
# output variable if present.
function(import_kconfig prefix kconfig_fragment)
  # Parse the lines prefixed with 'prefix' in ${kconfig_fragment}
  file(
    STRINGS
    ${kconfig_fragment}
    DOT_CONFIG_LIST
    REGEX "^${prefix}"
    ENCODING "UTF-8"
  )

  foreach (CONFIG ${DOT_CONFIG_LIST})
    # CONFIG could look like: CONFIG_NET_BUF=y

    # Match the first part, the variable name
    string(REGEX MATCH "[^=]+" CONF_VARIABLE_NAME ${CONFIG})

    # Match the second part, variable value
    string(REGEX MATCH "=(.+$)" CONF_VARIABLE_VALUE ${CONFIG})
    # The variable name match we just did included the '=' symbol. To just get the
    # part on the RHS we use match group 1
    set(CONF_VARIABLE_VALUE ${CMAKE_MATCH_1})

    if("${CONF_VARIABLE_VALUE}" MATCHES "^\"(.*)\"$") # Is surrounded by quotes
      set(CONF_VARIABLE_VALUE ${CMAKE_MATCH_1})
    endif()

    set("${CONF_VARIABLE_NAME}" "${CONF_VARIABLE_VALUE}" PARENT_SCOPE)
    list(APPEND keys "${CONF_VARIABLE_NAME}")
  endforeach()

  foreach(outvar ${ARGN})
    set(${outvar} "${keys}" PARENT_SCOPE)
  endforeach()
endfunction()

function(add_subdirectory_ifdef feature_toggle source_dir)
  if(${${feature_toggle}})
    add_subdirectory(${source_dir} ${ARGN})
  endif()
endfunction()

function(add_subdirectory_ifndef feature_toggle source_dir)
  if(NOT ${${feature_toggle}})
    add_subdirectory(${source_dir} ${ARGN})
  endif()
endfunction()

function(target_sources_ifdef feature_toggle target scope item)
  if(${${feature_toggle}})
    target_sources(${target} ${scope} ${item} ${ARGN})
  endif()
endfunction()

function(target_sources_ifndef feature_toggle target scope item)
  if(NOT ${${feature_toggle}})
    target_sources(${target} ${scope} ${item} ${ARGN})
  endif()
endfunction()

function(target_compile_definitions_ifdef feature_toggle target scope item)
  if(${${feature_toggle}})
    target_compile_definitions(${target} ${scope} ${item} ${ARGN})
  endif()
endfunction()

function(target_compile_definitions_ifndef feature_toggle target scope item)
  if(NOT ${${feature_toggle}})
    target_compile_definitions(${target} ${scope} ${item} ${ARGN})
  endif()
endfunction()

function(target_include_directories_ifdef feature_toggle target scope item)
  if(${${feature_toggle}})
    target_include_directories(${target} ${scope} ${item} ${ARGN})
  endif()
endfunction()

function(target_include_directories_ifndef feature_toggle target scope item)
  if(NOT ${${feature_toggle}})
    target_include_directories(${target} ${scope} ${item} ${ARGN})
  endif()
endfunction()

function(target_link_libraries_ifdef feature_toggle target item)
  if(${${feature_toggle}})
    target_link_libraries(${target} ${item} ${ARGN})
  endif()
endfunction()

function(target_link_libraries_ifndef feature_toggle target item)
  if(NOT ${${feature_toggle}})
    target_link_libraries(${target} ${item} ${ARGN})
  endif()
endfunction()

function(target_compile_options_ifdef feature_toggle target scope option)
  if(${feature_toggle})
    target_compile_options(${target} ${scope} ${option} ${ARGN})
  endif()
endfunction()

function(target_compile_options_ifndef feature_toggle target scope option)
  if(NOT ${feature_toggle})
    target_compile_options(${target} ${scope} ${option} ${ARGN})
  endif()
endfunction()

function(target_cc_options_ifdef feature_toggle target scope option)
  if(${${feature_toggle}})
    target_cc_options(${target} ${scope} ${option} ${ARGN})
  endif()
endfunction()

function(target_cc_options_ifndef feature_toggle target scope option)
  if(NOT ${${feature_toggle}})
    target_cc_options(${target} ${scope} ${option} ${ARGN})
  endif()
endfunction()

function(target_cc_option_fallback_ifdef feature_toggle target scope option1 option2)
  if(${${feature_toggle}})
    target_cc_option_fallback(${target} ${scope} ${option1} ${option2})
  endif()
endfunction()

function(target_cc_option_fallback_ifndef feature_toggle target scope option1 option2)
  if(NOT ${${feature_toggle}})
    target_cc_option_fallback(${target} ${scope} ${option1} ${option2})
  endif()
endfunction()

function(set_ifndef variable value)
  if(NOT ${variable})
    set(${variable} ${value} ${ARGN} PARENT_SCOPE)
  endif()
endfunction()

# Usage:
#   print(XXX)
#
# will print: "XXX: ${XXX}"
function(print arg)
  message(STATUS "${arg}: ${${arg}}")
endfunction()

# Usage:
#   assert(VAR "VAR not set.")
#
# will cause a FATAL_ERROR and print an error message if the first
# expression is false
macro(assert test comment)
  if(NOT ${test})
    message(FATAL_ERROR "Assertion failed: ${comment}")
  endif()
endmacro()

# Usage:
#   assert_not(OBSOLETE_VAR "OBSOLETE_VAR has been removed; use NEW_VAR instead")
#
# will cause a FATAL_ERROR and print an error message if the first
# expression is true
macro(assert_not test comment)
  if(${test})
    message(FATAL_ERROR "Assertion failed: ${comment}")
  endif()
endmacro()

# Usage:
#   assert_exists(CMAKE_READELF)
#
# will cause a FATAL_ERROR if there is no file or directory behind the
# variable
macro(assert_exists var)
  if(NOT EXISTS ${${var}})
    message(FATAL_ERROR "No such file or directory: ${var}: '${${var}}'")
  endif()
endmacro()

function(check_if_directory_is_writeable dir ok)
  execute_process(
    COMMAND
    ${PYTHON_EXECUTABLE}
    ${SEMINIX_SOURCE_DIR}/scripts/dir_is_writeable.py
    ${dir}
    RESULT_VARIABLE ret
    )

  if("${ret}" STREQUAL "0")
    # The directory is write-able
    set(${ok} 1 PARENT_SCOPE)
  else()
    set(${ok} 0 PARENT_SCOPE)
  endif()
endfunction()

function(find_appropriate_cache_directory dir)
  set(env_suffix_LOCALAPPDATA   .cache)

  set(env_suffix_HOME .cache)

  set(dirs XDG_CACHE_HOME HOME)

  foreach(env_var ${dirs})
    if(DEFINED ENV{${env_var}})
      set(env_dir $ENV{${env_var}})
      check_if_directory_is_writeable(${env_dir} ok)
      if(${ok})
        set(test_user_dir ${env_dir}/${env_suffix_${env_var}})
        # The directory is write-able
        set(user_dir ${test_user_dir})
        break()
      else()
        # The directory was not writeable, keep looking for a suitable
        # directory
      endif()
    endif()
  endforeach()

  # Populate local_dir with a suitable directory for caching
  # files. Prefer a directory outside of the git repository because it
  # is good practice to have clean git repositories.
  if(DEFINED user_dir)
    # Seminix's cache files go in the "seminix" subdirectory of the
    # user's cache directory.
    set(local_dir ${user_dir}/seminix)
  else()
    set(local_dir ${SEMINIX_SOURCE_DIR}/.cache)
  endif()

  set(${dir} ${local_dir} PARENT_SCOPE)
endfunction()

function(seminix_get_include_directories_for_lang lang i lib_name)
  seminix_get_parse_args(args ${ARGN})
  get_property(flags TARGET ${lib_name} PROPERTY INTERFACE_INCLUDE_DIRECTORIES)

	process_flags(${lang} flags output_list)
	string(REPLACE ";" "$<SEMICOLON>" genexp_output_list "${output_list}")

	if(NOT ARGN)
		set(result_output_list "-I$<JOIN:${genexp_output_list},$<SEMICOLON>-I>")
	elseif(args_STRIP_PREFIX)
		# The list has no prefix, so don't add it.
		set(result_output_list ${output_list})
	elseif(args_DELIMITER)
		set(result_output_list "-I$<JOIN:${genexp_output_list},${args_DELIMITER}-I>")
	endif()
	set(${i} ${result_output_list} PARENT_SCOPE)
endfunction()

function(process_flags lang input output)
	# The flags might contains compile language generator expressions that
	# look like this:
	# $<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>
	# $<$<COMPILE_LANGUAGE:CXX>:$<OTHER_EXPRESSION>>
	#
	# Flags that don't specify a language like this apply to all
	# languages.
	#
	# See COMPILE_LANGUAGE in
	# https://cmake.org/cmake/help/v3.3/manual/cmake-generator-expressions.7.html
	#
	# To deal with this, we apply a regex to extract the flag and also
	# to find out if the language matches.
	#
	# If this doesn't work out we might need to ban the use of
	# COMPILE_LANGUAGE and instead partition C, CXX, and ASM into
	# different libraries
	set(languages C CXX ASM)

	set(tmp_list "")

	foreach(flag ${${input}})
		set(is_compile_lang_generator_expression 0)
		foreach(l ${languages})
			if(flag MATCHES "<COMPILE_LANGUAGE:${l}>:([^>]+)>")
				set(updated_flag ${CMAKE_MATCH_1})
				set(is_compile_lang_generator_expression 1)
				if(${l} STREQUAL ${lang})
					# This test will match in case there are more generator expressions in the flag.
					# As example: $<$<COMPILE_LANGUAGE:C>:$<OTHER_EXPRESSION>>
					#             $<$<OTHER_EXPRESSION:$<COMPILE_LANGUAGE:C>:something>>
					string(REGEX MATCH "(\\\$<)[^\\\$]*(\\\$<)[^\\\$]*(\\\$<)" IGNORE_RESULT ${flag})
					if(CMAKE_MATCH_2)
						# Nested generator expressions are used, just substitue `$<COMPILE_LANGUAGE:${l}>` to `1`
						string(REGEX REPLACE "\\\$<COMPILE_LANGUAGE:${l}>" "1" updated_flag ${flag})
					endif()
					list(APPEND tmp_list ${updated_flag})
					break()
				endif()
			endif()
		endforeach()

		if(NOT is_compile_lang_generator_expression)
			# SHELL is used to avoid de-deplucation, but when process flags
			# then this tag must be removed to return real compile/linker flags.
			if(flag MATCHES "SHELL:[ ]*(.*)")
				separate_arguments(flag UNIX_COMMAND ${CMAKE_MATCH_1})
			endif()
			# Flags may be placed inside generator expression, therefore any flag
			# which is not already a generator expression must have commas converted.
			if(NOT flag MATCHES "\\\$<.*>")
				string(REPLACE "," "$<COMMA>" flag "${flag}")
			endif()
			list(APPEND tmp_list ${flag})
		endif()
	endforeach()

	set(${output} ${tmp_list} PARENT_SCOPE)
endfunction()

#
# 'toolchain_parse_make_rule' is a function that parses the output of
# 'gcc -M'.
#
# The argument 'input_file' is in input parameter with the path to the
# file with the dependency information.
#
# The argument 'include_files' is an output parameter with the result
# of parsing the include files.
function(toolchain_parse_make_rule input_file include_files)
	file(READ ${input_file} input)

	# The file is formatted like this:
	# empty_file.o: misc/empty_file.c \
	# nrf52840dk_nrf52840/nrf52840dk_nrf52840.dts \
	# nrf52840_qiaa.dtsi

	# Get rid of the backslashes
	string(REPLACE "\\" ";" input_as_list ${input})

	# Pop the first line and treat it specially
	list(GET input_as_list 0 first_input_line)
	string(FIND ${first_input_line} ": " index)
	math(EXPR j "${index} + 2")
	string(SUBSTRING ${first_input_line} ${j} -1 first_include_file)
	list(REMOVE_AT input_as_list 0)

	list(APPEND result ${first_include_file})

	# Add the other lines
	list(APPEND result ${input_as_list})

	# Strip away the newlines and whitespaces
	list(TRANSFORM result STRIP)

	set(${include_files} ${result} PARENT_SCOPE)
endfunction()

# Wrapper function around find_file that generates a fatal error if it isn't found
# Is equivalent to find_file except that it adds CMAKE_CURRENT_SOURCE_DIR as a path and sets
# CMAKE_FIND_ROOT_PATH_BOTH
function(require_file config_name file_name)
  find_file(
    ${config_name} "${file_name}"
    PATHS "${CMAKE_CURRENT_SOURCE_DIR}"
    CMAKE_FIND_ROOT_PATH_BOTH ${ARGV}
  )
  if("${${config_name}}" STREQUAL "${config_name}-NOTFOUND")
    message(FATAL_ERROR "Failed to find required file ${file_name}")
  endif()
  mark_as_advanced(FORCE ${config_name})
endfunction()

function(seminix_check_cache variable)
	cmake_parse_arguments(CACHE_VAR "REQUIRED;WATCH" "" "" ${ARGN})
	string(TOLOWER ${variable} variable_text)
	string(REPLACE "_" " " variable_text ${variable_text})

	get_property(cached_value CACHE ${variable} PROPERTY VALUE)

	# If the build has already been configured in an earlier CMake invocation,
	# then CACHED_${variable} is set. The CACHED_${variable} setting takes
	# precedence over any user or CMakeLists.txt input.
	# If we detect that user tries to change the setting, then print a warning
	# that a pristine build is needed.

	# If user uses -D<variable>=<new_value>, then cli_argument will hold the new
	# value, otherwise cli_argument will hold the existing (old) value.
	set(cli_argument ${cached_value})
	if(cli_argument STREQUAL CACHED_${variable})
		# The is no changes to the <variable> value.
		unset(cli_argument)
	endif()

	set(app_cmake_lists ${${variable}})
	if(cached_value STREQUAL ${variable})
		# The app build scripts did not set a default, The BOARD we are
		# reading is the cached value from the CLI
		unset(app_cmake_lists)
	endif()

	if(DEFINED CACHED_${variable})
		# Warn the user if it looks like he is trying to change the board
		# without cleaning first
		if(cli_argument)
			if(NOT ((CACHED_${variable} STREQUAL cli_argument) OR (${variable}_DEPRECATED STREQUAL cli_argument)))
				message(WARNING "The build directory must be cleaned pristinely when "
"changing ${variable_text},\n"
"Current value=\"${CACHED_${variable}}\", "
"Ignored value=\"${cli_argument}\"")
			endif()
		endif()

		if(CACHED_${variable})
			set(${variable} ${CACHED_${variable}} PARENT_SCOPE)
			# This resets the user provided value with previous (working) value.
			set(${variable} ${CACHED_${variable}} CACHE STRING "Selected ${variable_text}" FORCE)
		else()
			unset(${variable} PARENT_SCOPE)
			unset(${variable} CACHE)
		endif()
	elseif(cli_argument)
		set(${variable} ${cli_argument})

	elseif(DEFINED ENV{${variable}})
		set(${variable} $ENV{${variable}})

	elseif(app_cmake_lists)
		set(${variable} ${app_cmake_lists})

	elseif(${CACHE_VAR_REQUIRED})
		message(FATAL_ERROR "${variable} is not being defined on the CMake command-line in the environment or by the app.")
	endif()

	# Store the specified variable in parent scope and the cache
	set(${variable} ${${variable}} PARENT_SCOPE)
	set(CACHED_${variable} ${${variable}} CACHE STRING "Selected ${variable_text}")

	if(CACHE_VAR_WATCH)
		# The variable is now set to its final value.
		seminix_boilerplate_watch(${variable})
	endif()
endfunction()

# Usage:
#   seminix_boilerplate_watch(SOME_BOILERPLATE_VAR)
#
# Inform the build system that SOME_BOILERPLATE_VAR, a variable
# handled in cmake/app/boilerplate.cmake, is now fixed and should no
# longer be changed.
#
# This function uses variable_watch() to print a noisy warning
# if the variable is set after it returns.
function(seminix_boilerplate_watch variable)
	variable_watch(${variable} seminix_variable_set_too_late)
endfunction()

function(seminix_variable_set_too_late variable access value current_list_file)
	if(access STREQUAL "MODIFIED_ACCESS")
		message(WARNING
"
	 **********************************************************************
	 *
	 *                    WARNING
	 *
	 * CMake variable ${variable} set to \"${value}\" in:
	 *     ${current_list_file}
	 *
	 * This is too late to make changes! The change was ignored.
	 *
	 * Hint: ${variable} must be set before calling find_package(seminix ...).
	 *
	 **********************************************************************
")
	endif()
endfunction()

function(kernel_compile_definitions)
  target_compile_definitions(kernel_interface INTERFACE ${ARGV})
endfunction()

function(kernel_cc_options)
  target_cc_options(kernel_interface INTERFACE ${ARGV})
endfunction()

function(kernel_compile_definitions)
  target_compile_definitions(kernel_interface INTERFACE ${ARGV})
endfunction()
